package il.technion.cs236369.webserver;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.nio.file.AccessDeniedException;
import java.util.LinkedList;
import java.util.Queue;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.params.HttpParams;
import org.apache.http.util.EntityUtils;


public class SocketReader extends Thread{

	//each thread receives a serial number - for debug purposes
	private int ThreadNum = 0;
	//holds the socket queue
	private Queue<Socket> sockets = new LinkedList<Socket>();
	//holds the request queue
	private Queue<ExtendedHttpRequest> requests = new LinkedList<ExtendedHttpRequest>();
	//hold the current socket being handled by the socket reader 
	private Socket currentSocket = null;
	//used for binding to the sockets
	private HttpParams params;	
	
	public SocketReader(Queue<Socket> socketQueue, Queue<ExtendedHttpRequest> requestsQueue, HttpParams par, int ThreadNumber){
		sockets = socketQueue;
		requests = requestsQueue;
		params = par;
		ThreadNum = ThreadNumber;
	}

	/**
	 * listen to the socket queue, pops a socket and extract a request from it
	 */
	@Override
	public void run()
	{
		ExtendedHttpRequest request = null;
		while (true)
		{
			currentSocket = null;
			//get socket from queue
			getSocket();
			try
			{
				//parse request
				request = getRequestFromSocket();
				if (request != null)
				{
					//if parsed request successfully puts request into the queue
					synchronized (requests)
					{
						if (requests.size()<WebServer.SizeRequestQueue)
						{
							requests.add(request);
							requests.notify();
						} else
						{
							System.err.println("Request error is full");
							HttpResponser responsd = new HttpResponser();
							responsd.sendErrorResponse(request.getSocket(), 503,"Service Unavailable","Server is temporarily unavailable, please try again later");
						}
					}
				} else
				{
					//no need to keep socket open for the request handler, closes the connection
					if (currentSocket != null)
					{
						try
						{
							currentSocket.close();
						} catch (IOException e)
						{
							System.err.println("Can't close socket " + currentSocket.toString() );
						}
					}
				}
			}
			catch (Exception e)
			{
				System.err.println(e.getStackTrace());
				System.err.println(e.getCause());
				e.printStackTrace();
			}
		}
	}

	/**
	 * creates an http request from the current handled socket
	 * @return 
	 * @return HttpRequest (Class defined in this project)
	 */
	private ExtendedHttpRequest getRequestFromSocket()
	{
		if (currentSocket==null)
			return null;

		DefaultHttpServerConnection serverConn = new DefaultHttpServerConnection();
		ExtendedHttpRequest request =null;
		try
		{
			serverConn.bind(this.currentSocket, params);

			//set time out for each connection to 15 seconds
			serverConn.setSocketTimeout(45000);
		} catch(IOException e)
		{
			System.err.println("Unable to bind to client");
			return null;
		}
		try
		{
			// Receive connection request
			HttpRequest requestHeaders = serverConn.receiveRequestHeader();
			String methodType = requestHeaders.getRequestLine().getMethod();
			if ( methodType.contains("GET") ){
				request = ParseGetRequest(serverConn, requestHeaders);
				return request;
			} else
			{
				request = ParsePostRequest(serverConn, requestHeaders); 
				return request; 
			}
		}
		catch( AccessDeniedException e)
		{
			HttpResponser responsd = new HttpResponser();
			responsd.sendErrorResponse(currentSocket, 403,"Forbidden",e.getMessage() );
			System.err.println(e.getMessage());
			return null;
		}
		catch( FileNotFoundException e)
		{
			HttpResponser responsd = new HttpResponser();
			responsd.sendErrorResponse(currentSocket, 404,"Not Found",e.getMessage() );
			System.err.println(e.getMessage());
			return null;
		}
		//in case of errors closes the connection and return null
		catch (IOException e)
		{
			System.err.print("IO Error: ");
			System.err.println(e.getMessage());
			e.printStackTrace(System.err);
			HttpResponser respond = new HttpResponser();
			respond.sendErrorResponse(currentSocket,400, "Bad Request", "Cannot create connection");
			return null;
		}
		catch (HttpException e)
		{
			System.err.print("HTTP Error: ");
			e.printStackTrace();	        		
			HttpResponser respond = new HttpResponser();
			respond.sendErrorResponse(currentSocket,400, "Bad Request", "Cannot create connection");
			return null;
		} finally
		{
			try
			{
				//in case of a failure, no need to keep to connection option, close the connection
				if ( request  == null)
				{
					serverConn.close();
					System.err.println("connection closed - request is empty");
				}
			} catch (IOException e)
			{
				System.err.println("IO Error: "+e.getMessage());
				e.printStackTrace();
			}
		}
	}

	/**
	 * 
	 * @param serverConn - connection to receive the request body
	 * @param httpRequest - contains headers 
	 * @return
	 * @throws FileNotFoundException 
	 * @throws AccessDeniedException 
	 */
	private ExtendedHttpRequest ParsePostRequest(DefaultHttpServerConnection serverConn,HttpRequest httpRequest) {
		
		//creates the few of the fields of the ExtendedHttpRequest
		ExtendedHttpRequest request = new ExtendedHttpRequest(currentSocket,serverConn,httpRequest);
		request.setTypeToPostResponse();
		String path = "/";
		try {
			path = ExtractUrlFromString(httpRequest);
		} catch (AccessDeniedException e) {
			e.printStackTrace();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		request.setPath(path);
		//get the parameters of the request - adds parameters to the ExtendedHttpRequest
		request = ExtractParamsFromBody(serverConn,httpRequest,request);
		return request;
		
	}
	/**
	 * Extracts parameters from an httpRequest body and puts into a ExtendedHttpRequest
	 * @param serverConn
	 * @param httpRequest
	 * @param request
	 * @return
	 */
	private ExtendedHttpRequest ExtractParamsFromBody(DefaultHttpServerConnection serverConn, HttpRequest httpRequest, ExtendedHttpRequest request) {
		try {
			//to get the body of the request need to wrap the http request
			HttpEntityEnclosingRequest enclosingRequest = (HttpEntityEnclosingRequest) httpRequest;
			serverConn.receiveRequestEntity(enclosingRequest);
			HttpEntity entity = enclosingRequest.getEntity();
			//using a reader to read the body according to the UTF-8 encoding
			Reader contentReader = new BufferedReader(new InputStreamReader(entity.getContent(),"UTF-8"));
			StringBuilder parsedContent = new StringBuilder();
			int numChar=0;
			while ((numChar = contentReader.read())!=-1){
				parsedContent.append(Character.toChars(numChar));
			}
			contentReader.close();
			EntityUtils.consume(entity);
			//Takes the body content (after parsing) and extracts all parameters
			AddParamatersToRequest(parsedContent.toString(),request);
			return request;
		} catch (IOException | HttpException e  ) {
			HttpResponser respond = new HttpResponser();
			respond.sendErrorResponse(currentSocket,400, "Bad Request", "connection failed to extract body");
			return null;
		}
	}
	
	/**
	 * creates a request from a get request
	 * @param serverConn
	 * @param httpRequest
	 * @return
	 */
	private ExtendedHttpRequest ParseGetRequest(DefaultHttpServerConnection serverConn,HttpRequest httpRequest) throws AccessDeniedException, FileNotFoundException
	{
		ExtendedHttpRequest request = new ExtendedHttpRequest(currentSocket,serverConn,httpRequest);
		request.setTypeToGetResponse();
		String path = ExtractUrlFromString(httpRequest);
		request.setPath(path);
		if (httpRequest.getRequestLine().getUri().split("[?]").length>1){
			String parameters = httpRequest.getRequestLine().getUri().split("[?]")[1];
			AddParamatersToRequest(parameters,request);
		}
		return request;
	}

	private String ExtractUrlFromString(HttpRequest httpRequest) throws AccessDeniedException, FileNotFoundException
	{
		String path = httpRequest.getRequestLine().getUri().split("[?]")[0].toLowerCase();
		String basedirNoEnding = WebServer.baseDirPath.substring(0,WebServer.baseDirPath.length()-2);
		//in the case the url is absolute, test the the user refers to the root directory
		if (path.contains(":"))
		{
			path = path.substring(1,path.length());
			path = path.replace("/", "\\");
			basedirNoEnding = WebServer.baseDirPath.substring(0,WebServer.baseDirPath.length()-2);
			if (!path.startsWith(basedirNoEnding))
				throw new AccessDeniedException("Access is allowed to files under " + WebServer.baseDirPath + "direcotry only\n <br/> \"" + path + "\" is illegal");
		}		
		else
		{
			// handles relative urls
			path = path.replace("/", "\\");
			path = path.substring(1,path.length());
			path = WebServer.baseDirPath + path;
		}
		//if user calls the webserver only (localhost/8080) returns index page
		if (path.equals("") || path.equals("/") || path.equals(WebServer.baseDirPath) || path.equals(basedirNoEnding))
		{
			path = WebServer.baseDirPath + WebServer.WelcomeFile;
		}
		
		//test whether file/direcotry exists and is readable
		File f = new File(path);
		if (!f.exists())
			throw new FileNotFoundException("The file or directory do not exist");
		if (!f.canRead())
			throw new AccessDeniedException("Can't read files - you do not have premission");
		//The can read didn't work on files so this part fixes it
		if (f.isFile())
		{
			try
			{
				BufferedReader reader = new BufferedReader( new FileReader (f));
			} catch (FileNotFoundException e)
			{
				throw new AccessDeniedException("Can't read files - you do not have premission");
			}
		}
		
		return path;
	}
	
	/**
	 * receives an ExtendedHttpRequest and a string containing a request parameters and parses the parameters into the request  
	 * @param parameters
	 * @param request
	 * @return
	 */
	private ExtendedHttpRequest AddParamatersToRequest(String parameters, ExtendedHttpRequest request) {
		if (parameters==null) 
			return request;
		String[] brokenParamaters = parameters.split("[&]");
		for (String parameter: brokenParamaters){
			try{
			if (parameter.split("[=]").length>=2){
				String field = java.net.URLDecoder.decode(parameter.split("[=]")[0],"UTF-8");
				String value = java.net.URLDecoder.decode(parameter.split("[=]")[1],"UTF-8");
				System.out.println(field + ": " + value);
				request.setParameter(field,value);
			}
			}catch (UnsupportedEncodingException e) {
				HttpResponser respond = new HttpResponser();
				respond.sendErrorResponse(currentSocket, 400, "Bad Request","Illegal enconding!");
				return request;
			}
		}
		return request;
	}

	/**
	 * listen to the sockets queue, when not empty retrieves a socket and removes it from queue
	 */
	private void getSocket() {
		synchronized (sockets) {
			while (sockets.isEmpty()) {
				try {
					sockets.wait();
				} catch (InterruptedException | IllegalMonitorStateException e) {
					//System.err.println(ThreadNum + " was interrupted");
					e.printStackTrace();
				}
			}
			currentSocket = sockets.remove();
			//System.err.println(ThreadNum + " was interrupted");
		
		}
	}
}
